package org.vaadin.vaadinjit;

import java.util.HashMap;
import java.util.Map;

import org.vaadin.vaadinjit.data.JitData;

import com.vaadin.terminal.PaintException;
import com.vaadin.terminal.PaintTarget;
import com.vaadin.ui.AbstractComponent;


/**
 * Server-side base class for all visualizations. Implements functionality
 * common to all visualizations.
 * 
 * @author jonkka
 *
 */
public abstract class JitComponent<T extends JitData> extends AbstractComponent {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	private static final String DEFAULT_BACKGROUND_COLOR = "#1a1a1a";
	
	private static final String DEFAULT_TEXT_COLOR = "#ffffff";
	
	protected static int MIN_SIZE = 200;
	
	private Map<String, Object> options = new HashMap<String, Object>();
	
	/** The visualization data */
	protected T vizData;

	/** Tells if the component has been initialized */
	private boolean initialized = false;
	
	/** Tells repaint to initialize the visualization */
	private boolean init = false;
	
	/** Tells if the data has been updated */
	private boolean dataChanged = false;
	
	private boolean newDataLoaded = false;
	
	/** 
	 * The component will update data immediately when a change
	 * happens if this is true.
	 */
	private boolean updateImmediately = true;

	/** */
	private String backgroundColor = DEFAULT_BACKGROUND_COLOR;
	
	/** */
	private String textColor = DEFAULT_TEXT_COLOR;
	
	/** */
	private boolean colorChanged = true;
	
	/**
	 * 
	 */
	public JitComponent() {
		// Initialize the component to the minimum width
		this.setWidth(MIN_SIZE + "px");
		this.setHeight(MIN_SIZE + "px");
		
	}

	
	/**
	 * Specify that teh component should be updated immediately when data is
	 * changed
	 * 
	 * @param value
	 */
	public void setUpdateImmediately(boolean value) {
		this.updateImmediately = value;
	}
	
	
	/**
	 * Tells if the visualization updates immediately when data is changed
	 * 
	 * @return true if the visualization is immediate, else false
	 */
	public boolean getUpdateImmediately() {
		return updateImmediately;
	}
	
	/**
	 * Set visualization data
	 * 
	 * @param jitData the data
	 */
	public void setJitData(T jitData) {
		this.vizData = jitData;
	}
	
	
	/**
	 * Get the loaded data
	 * 
	 * @return the data
	 */
	public T getJitData() {
		return vizData;
	}


	/**
	 * <p>
	 * This tells the client to initialize the visualization.
	 * </p>
	 * 
	 * <p>
	 * This method exists because some JIT options are only available before the 
	 * visualization has been initialized. And it would not be practical to include 
	 * them all in the constructor.
	 * </p>
	 */
	public void init() {
		this.initialized = true;
		this.init = true;
		this.requestRepaint();
	}
	
	
	/**
	 * Tells if the visualization has been initialized yet.
	 * 
	 * @return true if the component has been initialized, else false
	 */
	public boolean isInitialized() {
		return initialized;
	}
	
	
	/**
	 * Get the current background color
	 * 
	 * @return a string representing the background color
	 */
	public String getBackgroundColor() {
		return backgroundColor;
	}


	/**
	 * Set the the background color. The specified color should be
	 * in HTML hex format.
	 * 
	 * @param backgroundColor the color
	 */
	public void setBackgroundColor(String backgroundColor) {
		this.backgroundColor = backgroundColor;
		this.colorChanged = true;
		this.requestRepaint();
	}


	/**
	 * Get the text color
	 * 
	 * @return the current text color
	 */
	public String getTextColor() {
		return textColor;
	}


	/**
	 * Set the text color of the visualization
	 * 
	 * @param textColor the text color
	 */
	public void setTextColor(String textColor) {
		this.textColor = textColor;
		this.colorChanged = true;
		this.requestRepaint();
	}



	/**
	 * Paint the component to the client-side
	 * 
	 * {@inheritDoc}
	 */
	@Override
	public void paintContent(PaintTarget target) throws PaintException {
		super.paintContent(target);
		
		// Return if the component has not been initialized
		if (!initialized)
			return;

		if (colorChanged) {
			target.addAttribute("backgroundColor", backgroundColor);
			target.addAttribute("textColor", textColor);
		}
		
		// Initialize the component
		if (init) {
			// Start a new tag and call the initialize method
			target.startTag("config");
			paintConfiguration(target, options);
			target.endTag("config");
			
			target.startTag("init");
			initialize(target);
			target.endTag("init");
			
			// Set init to false to not run init on every repaint
			init = false;
		}
		
		
		if (dataChanged) {
			target.addAttribute("jitData", vizData.toJSON());
			if (newDataLoaded) {
				target.addAttribute("newData", true);
				newDataLoaded = false;
			}
			dataChanged = false;
		}

	}
	
	

	private void paintConfiguration(PaintTarget target,
			Map<String, Object> opts) throws PaintException {

		String[] optionNames = opts.keySet().toArray(new String[]{});
		String[] optionTypes = new String[optionNames.length];
		int i = 0;
		
		for (String key : optionNames) {
			Object value = opts.get(key);
			
			if (value instanceof String) {
				target.addAttribute(key, (String)value);
				optionTypes[i++] = "string";
			}
			else if (value instanceof Double) {
				target.addAttribute(key, (Double)value);
				optionTypes[i++] = "double";
			}
			else if (value instanceof Integer) {
				target.addAttribute(key, (Integer)value);
				optionTypes[i++] = "int";
			}
			else if (value instanceof Boolean) {
				target.addAttribute(key, (Boolean)value);
				optionTypes[i++] = "boolean";
			}
			else {
				// Should never happen...
				throw new RuntimeException("Value of unexpected type encountered");
			}	
		}
		target.addAttribute("optionNames", optionNames);
		target.addAttribute("optionTypes", optionTypes);
	}



	/**
	 * Receive and handle events and other variable changes from the client.
	 * 
	 * {@inheritDoc}
	 */
	@Override
	public void changeVariables(Object source, Map<String, Object> variables) {
		super.changeVariables(source, variables);
	}
	
	
	/**
	 * Set an option to be used in the JIT constructor.
	 * 
	 * @param propertyName the name of the option
	 * @param propertyValue the value of the option
	 */
	public void setOption(String propertyName, String propertyValue) {
		if (initialized) {
			throw new IllegalStateException(
					"Options has to be set before initializing the component");
		}
		options.put(propertyName, propertyValue);
	}
	
	
	/**
	 * Set an option to be used in the JIT constructor.
	 * 
	 * @param propertyName the name of the option
	 * @param propertyValue the value of the option
	 */
	public void setOption(String propertyName, double propertyValue) {
		if (initialized) {
			throw new IllegalStateException(
					"Options has to be set before initializing the component");
		}
		options.put(propertyName, propertyValue);
	}
	
	
	/**
	 * Set an option to be used in the JIT constructor.
	 * 
	 * @param propertyName the name of the option
	 * @param propertyValue the value of the option
	 */
	public void setOption(String propertyName, int propertyValue) {
		if (initialized) {
			throw new IllegalStateException(
					"Options has to be set before initializing the component");
		}
		options.put(propertyName, propertyValue);
	}
	
	
	/**
	 * Set an option to be used in the JIT constructor.
	 * 
	 * @param propertyName the name of the option
	 * @param propertyValue the value of the option
	 */
	public void setOption(String propertyName, boolean propertyValue) {
		if (initialized) {
			throw new IllegalStateException(
					"Options has to be set before initializing the component");
		}
		options.put(propertyName, propertyValue);
	}
		
	
	/**
	 * Visualizations override this method to customize the initialization behavior.
	 * When overriding this method the super method should always be called.
	 * 
	 * @param target
	 * @throws PaintException
	 */
	protected void initialize(PaintTarget target) throws PaintException {
		
	}
	
	
	/**
	 * Tells the component that the data has changed and requests a repaint.
	 * 
	 */
	protected void setDataChanged() {
		this.dataChanged = true;
		this.requestRepaint();
	}
	
	/**
	 * Tell the component that entirely new data has been loaded
	 */
	protected void setNewDataLoaded() {
		setDataChanged();
		this.newDataLoaded = true;
		this.requestRepaint();
	}
}
